#define _GNU_SOURCE
#define _FILE_OFFSET_BITS 64

#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>
#include <unistd.h>
#include <sys/time.h>
#include <time.h>
#include <error.h>
#include <errno.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <malloc.h>
#include <string.h>
#include <math.h>
#include <limits.h>

#define ERROR(FMT, ...)	error_at_line(2, errno,	__FILE__, __LINE__, FMT, ## __VA_ARGS__)

#define VERBOSE(FMT, ...) if (verbose) fprintf(stderr, FMT"\n", ## __VA_ARGS__)

int delay = 0;
int period = 60;
int pass = 1;
long long record = 4096;
long long size = 0;
char *file = NULL;
int verbose = 0;
int shuffle = 0;
int init_pass = 0;
int random_start = 0;
long long limit = 0;
int fd;
int flags = O_CREAT | O_RDWR | O_LARGEFILE;
int wait_sync = 0;
int time_shift;
char *buf;

off_t offset;

double now(struct timeval *tv)
{
	if (gettimeofday(tv, NULL))
		ERROR("gettimeofday");

	return (double)tv->tv_sec
		+ (double)tv->tv_usec / 1000000.;
}

void io_write(void)
{
	if (write(fd, buf, record) != record)
		ERROR("write");
	offset += record;
}

void io_read(void)
{
	if (read(fd, buf, record) != record)
		ERROR("read");
	offset += record;
}

void seek_random()
{
	unsigned long long r;

	r = random();
	r <<=16;
	r ^= random();
	r <<=16;
	r ^= random();
	r <<=16;
	r ^= random();

	offset = (r / record) % (size / record) * record;

	if (lseek(fd, offset, SEEK_SET) == (off_t)-1)
		ERROR("lseek %lld", (long long)offset);
}

void seek_round()
{
	if (offset + record <= size)
		return;

	init_pass = 0;
	offset = 0;
	if (lseek(fd, offset, SEEK_SET) == (off_t)-1)
		ERROR("lseek %lld", (long long)offset);
}

void (*op_io)(void) = io_read;
void (*op_seek)(void) = seek_round;

void usage (void)
{
	fprintf(stderr, " usage: ioop [KEY]... <filename>\n"
			"\n"
			"      -v                   be verbose\n"
			"      -p <float>[sSmMhH]   pass time\n"
			"      -c <int>             pass count\n"
			"      -d <float>[sSmMhH]   initial delay\n"
			"      -b <float>[kKmMgG]   io block size\n"
			"      -s <float>[kKmMgG]   file size\n"
			"      -l <float>[kKmMgG]   limit real speed\n"
			"      -w                   write test\n"
			"      -r                   random seek after op\n"
			"      -R                   random start position\n"
			"      -I <int>             random generator state\n"
			"      -i                   process whole file at least once\n"
			"      -S                   sync timer\n"
			"      -D                   use O_DIRECT\n"
			"      -h                   display this mesage\n"
			"\n"
			" examples:\n"
			"   init file       : ioop -D -i -w -b 64k -s 1g test.tmp\n"
			" random benchmark  : ioop -D -p 1m -P 10 -r -b 4k test.tmp\n"
			"\n"
			" output format  : <effective speed in bytes per second> <real speed b/s>\n"
			"           <expected delay in seconds> <delay standard deviation in sec>\n"
			"\n");
}

long long parse_size(const char *s)
{
	char *p;
	double v;

	v = strtod(s, &p);
	switch (*p) {
		case 'k':
		case 'K':
			v *= 1<<10;
			break;
		case 'm':
		case 'M':
			v *= 1<<20;
			break;
		case 'g':
		case 'G':
			v *= 1<<30;
			break;
		case '\0':
			break;
		default:
			ERROR("invalid arg \"%f%c\"", v, *p);
	}
	return v;
}

int parse_time(const char *s)
{
	char *p;
	double v;

	v = strtod(s, &p);
	switch (*p) {
		case 's':
		case 'S':
			break;
		case 'm':
		case 'M':
			v *= 60;
			break;
		case 'h':
		case 'H':
			v *= 60*60;
			break;
		case 'd':
		case 'D':
			v *= 24*60*60;
			break;
		case 'w':
		case 'W':
			v *= 7*24*60*60;
		case '\0':
			break;
		default:
			ERROR("invalid arg \"%f%c\"", v, *p);
	}
	return v;
}


int main (int argc, char ** argv)
{
	int opt;
	struct timeval start, current;

	now(&current);
	srandom(current.tv_usec);

	while ((opt = getopt(argc, argv, "-vkp:c:d:b:s:l:f:I:wrRiSDh")) != -1) {
		switch (opt) {
			case 'd':
				delay = parse_time(optarg);
				break;
			case 'p':
				period = parse_time(optarg);
				break;
			case 'c':
				pass = atoi(optarg);
				break;
			case 'b':
				record = parse_size(optarg);
				break;
			case 's':
				size = parse_size(optarg);
				break;
			case 'l':
				limit = parse_size(optarg);
				break;
			case 'v':
				verbose = 1;
				break;
			case 'w':
				op_io = io_write;
				break;
			case 'r':
				op_seek = seek_random;
				break;
			case 'R':
				random_start = 1;
				break;
			case 'i':
				init_pass = 1;
				break;
			case 'I':
				srandom(atoi(optarg));
				break;
			case 'S':
				wait_sync = 1;
				break;
			case 'D':
				flags |= O_DIRECT;
				break;
			case 'h':
				usage();
				return 0;
			case 1:
				file = optarg;
				break;
			case '?':
				usage();
				return 2;
		}
	}

	if (!file) {
		usage();
		return 2;
	}

	buf = valloc(record);
	if (!buf)
		ERROR("malloc");
	memset(buf, 42, record);

	VERBOSE("buffer addr: %p size: %lld", buf, record);

	fd = open(file, flags, 0666);
	if (fd < 0)
		ERROR("open");
	offset = 0;

	limit *= period;

	if (!size) {
		size = lseek(fd, 0, SEEK_END);
		if (size == (off_t)-1)
			ERROR("lseek");
		if (lseek(fd, 0, SEEK_SET) == (off_t)-1)
			ERROR("lseek");
	}
	size = size / record * record;

	VERBOSE("file \"%s\" fd %d size %lld", file, fd, size);

	if (delay) {
		VERBOSE("initial delay");
		sleep(delay);
	}

	now(&start);
	if (wait_sync) {
		VERBOSE("wait timer sync");
		time_shift = 0;
		do {
			usleep(50);
			now(&current);
		} while (current.tv_sec % period != time_shift
				|| current.tv_sec == start.tv_sec);
	} else {
		time_shift = start.tv_sec % period;
	}

	if (random_start) {
		seek_random();
		VERBOSE("random start %llx", (long long)offset);
	}

	VERBOSE("start %d pass * %d sec", pass, period);
	while (init_pass || pass--) {
		double  st, bg, cu, tm;
		long long bytes = 0;
		long ops_count = 0;
		double ops_time = 0;
		double var_time = 0;
		double exp_time;
		double dev_time;
		double real_speed, eff_speed;

		st = now(&start);
		do {
			if (limit) {
				int s;

				tm = now(&current) - st;
				tm = (double)bytes * period / limit - tm;

				s = tm;
				if (s > 0)
					sleep(s);
				else {
					s = tm * 1000000;
					if (s > 0)
						usleep(s);
				}
			}

			bg = now(&current);
			op_seek();
			op_io();
			cu = now(&current);

			tm = cu - bg;

			ops_time += tm;
			var_time += tm * tm;
			ops_count += 1;

			bytes += record;
		} while (current.tv_sec % period != time_shift
				|| current.tv_sec == start.tv_sec);

		tm = cu - st;

		exp_time = ops_time / ops_count;
		var_time = var_time / ops_count - exp_time * exp_time;
		dev_time = sqrt(var_time);

		real_speed = bytes / tm;
		eff_speed  = bytes / ops_time;

		VERBOSE("");
		VERBOSE(" %.2f Mb/s, %.2f Mb/s real, %lld bytes, 0x%llx off",
				eff_speed / (1<<20), real_speed / (1<<20),
				bytes, (long long)offset);
		VERBOSE(" %ld ops, %.2f sec, %.2f sec real, %.2f ms exp, %.2f ms dev",
				ops_count, ops_time, tm,
				exp_time * 1000., dev_time * 1000.);
		VERBOSE("");

		printf("%f %f %f %f\n",
				eff_speed,
				real_speed,
				exp_time,
				dev_time);
		fflush(stdout);
	}
	VERBOSE("done");
	return 0;
}
